﻿namespace EyeOpen.Data
{
    using System;
    using System.Collections.Generic;
    using System.Data.Common;
    using System.Reflection;
    using EyeOpen.Domain;

    public class DataMapper<T> : IDataMapper<T> where T : IUniqueObject
    {
        private string connectionString;
        
        private string tableName;

        private DbProviderFactory providerFactory;

        /// <summary>
        /// Construct a DataProvider provinding connection and table name.
        /// </summary>
        /// <param name="connection">The connection to use with the storage.</param>
        /// <param name="tableName">The table name</param>
        public DataMapper(string connectionString, DbProviderFactory providerFactory, string tableName)
        {
            this.connectionString = connectionString;
            this.providerFactory = providerFactory;
            this.tableName = tableName;
        }

        private string ConnectionString
        {
            get
            {
                return this.connectionString;
            }
        }

        private string TableName
        {
            get 
            { 
                return this.tableName; 
            }
        }

        private DbProviderFactory ProviderFactory
        {
            get 
            { 
                return this.providerFactory; 
            }
        }

        /// <summary>
        /// Return the element from the storage.
        /// </summary>
        /// <param name="id">The id of the object to return.</param>
        /// <returns></returns>
        public virtual T Get(Guid id)
        {
            IList<T> datas = this.GetAll(id);
            
            switch (datas.Count)
            {
                case 1:
                    return datas[0];

                case 0:
                    return default(T);

                default:
                    throw new Exception("The id specified is not a key.");
            }
        }

        /// <summary>
        /// Return all instances from storage.
        /// </summary>
        /// <returns>Return a collection of entities</returns>
        public virtual IList<T> GetAll()
        {
            return this.GetAll(Guid.Empty);
        }

        /// <summary>
        /// Execute a insert operation.
        /// </summary>
        /// <param name="value">The entity to insert.</param>
        public virtual void Insert(T value)
        {
            this.ExecuteOperation(value, DataOperation.Insert);
        }

        /// <summary>
        /// Execute an update operation.
        /// </summary>
        /// <param name="value">The entity to update.</param>
        public virtual void Update(T value)
        {
            this.ExecuteOperation(value, DataOperation.Update);
        }

        /// <summary>
        /// Execute a delete operation.
        /// </summary>
        /// <param name="value">The entity to delete.</param>
        public virtual void Delete(T value)
        {
            this.ExecuteOperation(value, DataOperation.Delete);
        }

        private IList<T> GetAll(Guid id)
        {
            string formattedSelect = null;

            if (id.Equals(Guid.Empty))
            {
                formattedSelect = "select * from {0}";
            }
            else
            {
                formattedSelect = "select * from {0} where id = @id";
            }

            string selectCommand = string.Format(formattedSelect, this.TableName);

            IList<T> datas = new List<T>();

            using (DbConnection connection = this.ProviderFactory.CreateConnection())
            {
                connection.ConnectionString = this.ConnectionString;

                using (DbCommand command = connection.CreateCommand())
                {
                    command.Connection = connection;
                    command.CommandText = selectCommand;
                    
                    if (!id.Equals(Guid.Empty))
                    {
                        command.AddParameter("@Id", id);
                    }

                    connection.Open();

                    DbDataReader reader = command.ExecuteReader();

                    while (reader.Read())
                    {
                        T obj = Activator.CreateInstance<T>();
                        for (int i = 0; i < reader.FieldCount; i++)
                        {
                            string propertyName = reader.GetName(i);
                            object value = reader[i];

                            if (reader[i] != System.DBNull.Value)
                            {
                                if (reader[i].GetType() == typeof(string))
                                {
                                    obj.GetType().GetProperty(propertyName).SetValue(obj, value.ToString().Trim(), null);
                                }
                                else
                                {
                                    obj.GetType().GetProperty(propertyName).SetValue(obj, value, null);
                                }
                            }
                        }

                        datas.Add(obj);
                    }
                }
            }

            return datas;
        }

        /// <summary>
        /// Execute a command on the storage.
        /// </summary>
        /// <param name="value">The entity to modify.</param>
        /// <param name="dataOperation">The operation to execute.</param>
        private void ExecuteOperation(T value, DataOperation dataOperation)
        {
            string formattedQuery = null;
            string insertParameters = string.Empty;
            string updateParemeters = string.Empty;
            string columns = string.Empty;
            string separator = ", ";

            DbCommand command = this.ProviderFactory.CreateConnection().CreateCommand();

            PropertyInfo[] properties = typeof(T).GetProperties();
            foreach (PropertyInfo property in properties)
            {
                columns += property.Name + separator;
                insertParameters += "@" + property.Name + separator;

                if (!property.Name.Equals("id", StringComparison.InvariantCultureIgnoreCase))
                {
                    updateParemeters += property.Name + "=@" + property.Name + separator;
                }

                if (property.Name.Equals("id", StringComparison.InvariantCultureIgnoreCase) && dataOperation == DataOperation.Delete)
                {
                    command.AddParameter("@" + property.Name, property.GetValue(value, null));
                }

                if (dataOperation != DataOperation.Delete)
                {
                    command.AddParameter("@" + property.Name, property.GetValue(value, null));
                }
            }

            columns = columns.TrimEnd(separator.ToCharArray());
            insertParameters = insertParameters.Remove(insertParameters.LastIndexOf(separator));
            updateParemeters = updateParemeters.Remove(updateParemeters.LastIndexOf(separator));

            switch (dataOperation)
            {
                case DataOperation.Insert:
                    formattedQuery = "insert into {0} ({1}) values({2})";
                    formattedQuery = string.Format(formattedQuery, this.TableName, columns, insertParameters);
                    break;

                case DataOperation.Update:
                    formattedQuery = "update {0} set {1} where id = @id";
                    formattedQuery = string.Format(formattedQuery, this.TableName, updateParemeters);
                    break;

                case DataOperation.Delete:
                    formattedQuery = "delete from {0} where id = @id";
                    formattedQuery = string.Format(formattedQuery, this.TableName);
                    break;
            }

            command.CommandText = formattedQuery;

            using (DbConnection connection = this.ProviderFactory.CreateConnection())
            {
                connection.ConnectionString = this.ConnectionString;

                using (command)
                {
                    connection.Open();
                    command.Connection = connection;
                    command.ExecuteNonQuery();
                }
            }
        }
    }
}